// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use assert_matches2::{assert_let, assert_matches};
use insta::assert_json_snapshot;
use matrix_sdk_common::deserialized_responses::{
    AlgorithmInfo, ProcessedToDeviceEvent, ToDeviceUnableToDecryptReason, VerificationLevel,
    VerificationState,
};
use matrix_sdk_test::{async_test, ruma_response_from_json};
use ruma::{
    events::AnyToDeviceEvent, room_id, serde::Raw, to_device::DeviceIdOrAllDevices, RoomId,
    TransactionId,
};
use serde_json::{json, value::to_raw_value, Value};

use crate::{
    machine::{
        test_helpers::{
            build_encrypted_to_device_content_without_sender_data, build_session_for_pair,
            get_machine_pair, get_machine_pair_with_session, get_prepared_machine_test_helper,
            receive_encrypted_to_device_test_helper,
            send_and_receive_encrypted_to_device_test_helper,
        },
        tests::{self, decryption_verification_state::mark_alice_identity_as_verified_test_helper},
    },
    olm::SenderData,
    session_manager::CollectStrategy,
    types::{
        events::{
            room::encrypted::ToDeviceEncryptedEventContent, EventType as _, ToDeviceCustomEvent,
            ToDeviceEvent,
        },
        requests::ToDeviceRequest,
    },
    utilities::json_convert,
    verification::tests::bob_id,
    CrossSigningBootstrapRequests, DecryptionSettings, DeviceData, EncryptionSettings,
    EncryptionSyncChanges, LocalTrust, OlmError, OlmMachine, TrustRequirement,
};

/// Happy path test: encrypt a to-device message, and check it is successfully
/// decrypted by the recipient, and that all the metadata is set as expected.
#[async_test]
async fn test_send_encrypted_to_device() {
    let (alice, bob) =
        get_machine_pair_with_session(tests::alice_id(), tests::user_id(), false).await;

    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    let decryption_settings =
        DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

    let processed_event = send_and_receive_encrypted_to_device_test_helper(
        &alice,
        &bob,
        custom_event_type,
        &custom_content,
        &decryption_settings,
    )
    .await;

    assert_let!(ProcessedToDeviceEvent::Decrypted { raw, encryption_info } = processed_event);

    let decrypted_event = raw.deserialize().unwrap();

    assert_eq!(decrypted_event.event_type().to_string(), custom_event_type.to_owned());

    let decrypted_value = to_raw_value(&raw).unwrap();
    let decrypted_value = serde_json::to_value(decrypted_value).unwrap();

    assert_eq!(
        decrypted_value.get("content").unwrap().get("device_id").unwrap().as_str().unwrap(),
        custom_content.get("device_id").unwrap().as_str().unwrap(),
    );

    assert_eq!(
        decrypted_value.get("content").unwrap().get("rooms").unwrap().as_array().unwrap(),
        custom_content.get("rooms").unwrap().as_array().unwrap(),
    );

    assert_eq!(encryption_info.sender, alice.user_id().to_owned());

    assert_matches!(&encryption_info.sender_device, Some(sender_device));
    assert_eq!(sender_device.to_owned(), alice.device_id().to_owned());

    assert_matches!(
        &encryption_info.algorithm_info,
        AlgorithmInfo::OlmV1Curve25519AesSha2 { curve25519_public_key_base64 }
    );
    let alice_device =
        alice.get_device(alice.user_id(), alice.device_id(), None).await.unwrap().unwrap();
    assert_eq!(
        curve25519_public_key_base64.to_owned(),
        alice_device.curve25519_key().unwrap().to_base64()
    );

    assert_matches!(
        &encryption_info.verification_state,
        VerificationState::Unverified(VerificationLevel::UnsignedDevice)
    );
}

/// Test what happens when the sending device is deleted before the to-device
/// event arrives. (It should still be successfully decrypted.)
///
/// Regression test for https://github.com/matrix-org/matrix-rust-sdk/issues/5768.
#[async_test]
async fn test_encrypted_to_device_from_deleted_device() {
    let (alice, bob) =
        get_machine_pair_with_session(tests::alice_id(), tests::user_id(), false).await;

    // Tell Bob that Alice's device has been deleted
    let mut keys_query_response = ruma::api::client::keys::get_keys::v3::Response::default();
    keys_query_response.device_keys.insert(alice.user_id().to_owned(), Default::default());
    bob.receive_keys_query_response(&TransactionId::new(), &keys_query_response).await.unwrap();

    let custom_event_type = "m.new_device";
    let custom_content = json!({"a": "b"});

    let decryption_settings =
        DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

    let processed_event = send_and_receive_encrypted_to_device_test_helper(
        &alice,
        &bob,
        custom_event_type,
        &custom_content,
        &decryption_settings,
    )
    .await;

    assert_let!(ProcessedToDeviceEvent::Decrypted { raw, encryption_info } = processed_event);

    let decrypted_event = raw.deserialize().unwrap();
    assert_eq!(decrypted_event.event_type().to_string(), custom_event_type.to_owned());

    assert_eq!(encryption_info.sender, alice.user_id().to_owned());
    assert_matches!(&encryption_info.sender_device, Some(sender_device));
    assert_eq!(sender_device.to_owned(), alice.device_id().to_owned());
}

/// If the sender device is genuinely unknown (it is not in the store, nor does
/// the to-device message contain `sender_device_keys`), decryption will fail,
/// with `EventError::MissingSigningKey`.
#[async_test]
async fn test_receive_custom_encrypted_to_device_with_no_sender_device_keys_fails_if_device_unknown(
) {
    let (bob, otk) = get_prepared_machine_test_helper(bob_id(), false).await;

    let alice = OlmMachine::new(tests::alice_id(), tests::alice_device_id()).await;

    let bob_device = DeviceData::from_machine_test_helper(&bob).await.unwrap();
    alice.store().save_device_data(&[bob_device]).await.unwrap();

    let (alice, bob) = build_session_for_pair(alice, bob, otk).await;

    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    let decryption_settings =
        DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

    // We need to suppress the sender_data field to correctly emulate an unknown
    // device
    let bob_device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
    let raw_encrypted = build_encrypted_to_device_content_without_sender_data(
        &alice,
        &bob_device.device_keys,
        custom_event_type,
        &custom_content,
    )
    .await;

    let processed_event = receive_encrypted_to_device_test_helper(
        alice.user_id(),
        &bob,
        &decryption_settings,
        Raw::new(&raw_encrypted).unwrap(),
    )
    .await;

    assert_let!(ProcessedToDeviceEvent::UnableToDecrypt { utd_info, .. } = processed_event);
    assert_eq!(utd_info.reason, ToDeviceUnableToDecryptReason::DecryptionFailure);
}

#[async_test]
async fn test_excluding_insecure_means_custom_to_device_events_from_unverified_devices_are_utd() {
    // Given we are in "exclude insecure devices" mode
    let decryption_settings = DecryptionSettings {
        sender_device_trust_requirement: TrustRequirement::CrossSignedOrLegacy,
    };

    // Bob is the receiver
    let (bob, otk) = get_prepared_machine_test_helper(bob_id(), false).await;

    // Alice is the sender
    let alice = OlmMachine::new(tests::alice_id(), tests::alice_device_id()).await;

    let bob_device = DeviceData::from_machine_test_helper(&bob).await.unwrap();
    alice.store().save_device_data(&[bob_device]).await.unwrap();

    let (alice, bob) = build_session_for_pair(alice, bob, otk).await;

    // And the receiving device does not consider the sending device verified
    make_alice_unverified(&alice, &bob).await;

    assert!(!bob
        .get_device(alice.user_id(), alice.device_id(), None)
        .await
        .unwrap()
        .unwrap()
        .is_verified());

    // When we send a custom event
    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    let processed_event = send_and_receive_encrypted_to_device_test_helper(
        &alice,
        &bob,
        custom_event_type,
        &custom_content,
        &decryption_settings,
    )
    .await;

    // Then it was not processed because the sending device was not verified
    assert_let!(ProcessedToDeviceEvent::UnableToDecrypt { utd_info, .. } = processed_event);

    // And the info provided in the UnableToDecrypt matches what we supplied
    assert_eq!(utd_info.reason, ToDeviceUnableToDecryptReason::UnverifiedSenderDevice);
}

#[async_test]
async fn test_excluding_insecure_does_not_prevent_key_events_being_processed() {
    // Given we are in "exclude insecure devices" mode
    let decryption_settings = DecryptionSettings {
        sender_device_trust_requirement: TrustRequirement::CrossSignedOrLegacy,
    };

    // Bob is the receiver
    let (bob, otk) = get_prepared_machine_test_helper(bob_id(), false).await;

    // Alice is the sender
    let alice = OlmMachine::new(tests::alice_id(), tests::alice_device_id()).await;

    let bob_device = DeviceData::from_machine_test_helper(&bob).await.unwrap();
    alice.store().save_device_data(&[bob_device]).await.unwrap();

    let (alice, bob) = build_session_for_pair(alice, bob, otk).await;

    // And the receiving device does not consider the sending device verified
    make_alice_unverified(&alice, &bob).await;

    assert!(!bob
        .get_device(alice.user_id(), alice.device_id(), None)
        .await
        .unwrap()
        .unwrap()
        .is_verified());

    // When we send a room key event
    let key_event =
        create_and_share_session_without_sender_data(&alice, &bob, room_id!("!23:s.co")).await;

    let key_event_content = serde_json::to_value(&key_event.content).unwrap();

    let processed_event = send_and_receive_encrypted_to_device_test_helper(
        &alice,
        &bob,
        "m.room_key",
        &key_event_content,
        &decryption_settings,
    )
    .await;

    // Then it was processed because even though the sending device was not
    // verified, room key events are allowed through.
    assert_matches!(processed_event, ProcessedToDeviceEvent::Decrypted { .. });
}

#[async_test]
async fn test_send_olm_encryption_info_unverified_identity() {
    let (alice, bob) =
        get_machine_pair_with_session(tests::alice_id(), tests::user_id(), false).await;

    // bootstrap cross-signing
    // tests::setup_cross_signing_for_machine_test_helper(&alice, &bob).await;

    // sign alice device and let bob knows about it
    tests::sign_alice_device_for_machine_test_helper(&alice, &bob).await;

    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    let decryption_settings =
        DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

    let processed_event = send_and_receive_encrypted_to_device_test_helper(
        &alice,
        &bob,
        custom_event_type,
        &custom_content,
        &decryption_settings,
    )
    .await;

    assert_let!(ProcessedToDeviceEvent::Decrypted { encryption_info, .. } = processed_event);
    assert_eq!(encryption_info.sender, alice.user_id().to_owned());
    assert_eq!(encryption_info.sender_device, Some(alice.device_id().to_owned()));
    assert_eq!(encryption_info.session_id(), None);

    assert_matches!(
        &encryption_info.verification_state,
        VerificationState::Unverified(VerificationLevel::UnverifiedIdentity)
    );
}

#[async_test]
async fn test_send_olm_encryption_info_verified_identity() {
    let (alice, bob) =
        get_machine_pair_with_session(tests::alice_id(), tests::user_id(), false).await;

    // bootstrap cross-signing
    tests::setup_cross_signing_for_machine_test_helper(&alice, &bob).await;

    // sign alice device and let bob knows about it
    tests::sign_alice_device_for_machine_test_helper(&alice, &bob).await;

    // Given alice is verified
    mark_alice_identity_as_verified_test_helper(&alice, &bob).await;

    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    let decryption_settings =
        DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

    let processed_event = send_and_receive_encrypted_to_device_test_helper(
        &alice,
        &bob,
        custom_event_type,
        &custom_content,
        &decryption_settings,
    )
    .await;

    assert_let!(ProcessedToDeviceEvent::Decrypted { encryption_info, .. } = processed_event);
    assert_eq!(encryption_info.sender, alice.user_id().to_owned());
    assert_eq!(encryption_info.sender_device, Some(alice.device_id().to_owned()));
    assert_eq!(encryption_info.session_id(), None);

    assert_matches!(&encryption_info.verification_state, VerificationState::Verified);
}

#[async_test]
async fn test_send_olm_encryption_info_verified_locally() {
    let (alice, bob) =
        get_machine_pair_with_session(tests::alice_id(), tests::user_id(), false).await;

    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    bob.get_device(alice.user_id(), alice.device_id(), None)
        .await
        .unwrap()
        .unwrap()
        .set_local_trust(LocalTrust::Verified)
        .await
        .unwrap();

    let decryption_settings =
        DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

    let processed_event = send_and_receive_encrypted_to_device_test_helper(
        &alice,
        &bob,
        custom_event_type,
        &custom_content,
        &decryption_settings,
    )
    .await;

    assert_let!(ProcessedToDeviceEvent::Decrypted { encryption_info, .. } = processed_event);
    assert_eq!(encryption_info.sender, alice.user_id().to_owned());
    assert_eq!(encryption_info.sender_device, Some(alice.device_id().to_owned()));
    assert_eq!(encryption_info.session_id(), None);

    assert_matches!(&encryption_info.verification_state, VerificationState::Verified);
}

#[async_test]
async fn test_send_olm_encryption_info_verification_violation() {
    let (alice, bob) =
        get_machine_pair_with_session(tests::alice_id(), tests::user_id(), false).await;

    // bootstrap cross-signing
    tests::setup_cross_signing_for_machine_test_helper(&alice, &bob).await;

    // sign alice device and let bob knows about it
    tests::sign_alice_device_for_machine_test_helper(&alice, &bob).await;

    // Given alice is verified
    mark_alice_identity_as_verified_test_helper(&alice, &bob).await;

    // Reset alice identity
    alice.bootstrap_cross_signing(true).await.unwrap();
    tests::setup_cross_signing_for_machine_test_helper(&alice, &bob).await;
    tests::sign_alice_device_for_machine_test_helper(&alice, &bob).await;

    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    let decryption_settings =
        DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

    let processed_event = send_and_receive_encrypted_to_device_test_helper(
        &alice,
        &bob,
        custom_event_type,
        &custom_content,
        &decryption_settings,
    )
    .await;

    assert_let!(ProcessedToDeviceEvent::Decrypted { encryption_info, .. } = processed_event);
    assert_eq!(encryption_info.sender, alice.user_id().to_owned());
    assert_eq!(encryption_info.sender_device, Some(alice.device_id().to_owned()));
    assert_eq!(encryption_info.session_id(), None);

    assert_matches!(
        &encryption_info.verification_state,
        VerificationState::Unverified(VerificationLevel::VerificationViolation)
    );
}

#[async_test]
async fn test_processed_to_device_variants() {
    let (alice, bob) =
        get_machine_pair_with_session(tests::alice_id(), tests::user_id(), false).await;

    let custom_event_type = "rtc.call.encryption_keys";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "call_id": "",
            "keys": [
                {
                    "index": 0,
                    "key": "I7qrSrCR7Yo9B4iGVnR8IA"
                }
            ],
    });

    let device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
    let raw_encrypted = device
        .encrypt_event_raw(custom_event_type, &custom_content, CollectStrategy::AllDevices)
        .await
        .expect("Should have encrypted the content");

    let request = ToDeviceRequest::new(
        bob.user_id(),
        DeviceIdOrAllDevices::DeviceId(tests::bob_device_id().to_owned()),
        "m.room.encrypted",
        raw_encrypted.cast(),
    );

    let encrypted_event = ToDeviceEvent::new(
        alice.user_id().to_owned(),
        tests::to_device_requests_to_content(vec![request.clone().into()]),
    );

    let custom_event = json!({
            "sender": "@alice:example.com",
            "type": "m.new_device",
            "content": {
                "device_id": "XYZABCDE",
                "rooms": ["!726s6s6q:example.com"]
            }
    });
    let clear_event = serde_json::from_value::<ToDeviceCustomEvent>(custom_event.clone()).unwrap();

    let encrypted_event = json_convert(&encrypted_event).unwrap();
    let clear_event = json_convert(&clear_event).unwrap();

    let malformed_event_no_type = json!({
        "sender": "@alice:example.com",
        "content": {
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
        }
    });
    let malformed_event: Raw<AnyToDeviceEvent> =
        serde_json::from_value(malformed_event_no_type).unwrap();

    let alice_curve = alice
        .get_device(alice.user_id(), alice.device_id(), None)
        .await
        .unwrap()
        .unwrap()
        .curve25519_key()
        .unwrap();
    let bob_curve = bob
        .get_device(bob.user_id(), bob.device_id(), None)
        .await
        .unwrap()
        .unwrap()
        .curve25519_key()
        .unwrap();

    // let's add a UTD event
    let utd_event = json!({
        "content": {
            "algorithm": "m.olm.v1.curve25519-aes-sha2",
            "ciphertext": {
                bob_curve.to_base64(): {
                    // this payload is just captured from a sync of some other element web with other users
                    "body": "Awogjvpx458CGhuo77HX/+tp1sxgRoCi7iAlzMvfrpbWoREQAiKACysX/p+ojr5QitCi9WRXNyamW2v2LTvoyWKtVaA2oHnYGR5s5RYhDfnIgh5MMSqqKlAbfqLvrbLovTYcKagCBbFnbA43f6zYM44buGgy8q70hMVH5WP6aK1E9Z3DVZ+8PnXQGpsrxvz2IsL6w0Nzl/qUyBEQFcgkjoDPawbsZRCllMgq2LQUyqlun6IgDTCozqsfxhDWpdfYGde4z16m34Ang7f5pH+BmPrFs6E1AO5+UbhhhS6NwWlfEtA6/9yfMxWLz1d2OrLh+QG7lYFAU9/CzIoPxaHKKr4JxgL9CjsmUPyDymWOWHP0jLi1NwpOv6hGpx0FgM7jJIMk6gWGgC5rEgEeTIwdrJh3F9OKTNSva5hvD9LomGk6tZgzQG6oap1e3wiOUyTt6S7BlyMppIu3RlIiNihZ9e17JEGiGDXOXzMJ6ISAgvGVgTP7+EvyEt2Wt4du7uBo/UvljRvVNu3I8tfItizPAOlvz460+aBDxk+sflJWt7OnhiyPnOCfopb+1RzqKVCnnPyVaP2f4BPf8qpn/f5YZk+5jJgBrGPiHzzmb3sQ5pC470s6+U3MpVFlFTG/xPBtMRMwPsbKoHfnRPqIqGu5dQ1Sw7T6taDXWjP450TvjxgHK5t2z1rLA2SXzAB1P8xbi6YXqQwxL6PvMNHn/TM0jiIQHYuqg5/RKLyhHybfP8JAjgNBw9z16wfKR/YoYFr7c+S4McQaMNa8v2SxGzhpCC3duAoK2qCWLEkYRO5cMCsGm/9bf8Q+//OykygBU/hdkT1eHUbexgALPLdfhzduutU7pbChg4T7SH7euh/3NLmS/SQvkmPfm3ckbh/Vlcj9CsXws/7MX/VJbhpbyzgBNtMnbG6tAeAofMa6Go/yMgiNBZIhLpAm31iUbUhaGm2IIlF/lsmSYEiBPoSVfFU44tetX2I/PBDGiBlzyU+yC2TOEBwMGxBE3WHbIe5/7sKW8xJF9t+HBfxIyW1QRtY3EKdEcuVOTyMxYzq3L5OKOOtPDHObYiiXg00mAgdQqgfkEAIfoRCOa2NYfTedwwo0S77eQ1sPvW5Hhf+Cm+bLibkWzaYHEZF+vyE9/Tn0tZGtH07RXfUyhp1vtTH49OBZHGkb/r+L8OjYJTST1dDCGqeGXO3uwYjoWHXtezLVHYgL+UOwcLJfMF5s9DQiqcfYXzp2kEWGsaetBFXcUWqq4RMHqlr6QfbxyuYLlQzc/AYA/MrT3J6nDpNLcvozH3RcIs8NcKcjdtjvgL0QGThy3RcecJQEDx3STrkkePL3dlyFCtVsmtQ0vjBBCxUgdySfxiobGGnpezZYi7q+Xz61GOZ9QqYmkcZOPzfNWeqtmzB7gqlH1gkFsK2yMAzKq2XCDFHvA7YAT3yMGiY06FcQ+2jyg7Bk2Q+AvjTG8hlPlmt6BZfW5cz1qx1apQn1qHXHrgfWcI52rApYQlNPOU1Uc8kZ8Ee6XUhhXBGY1rvZiKjKFG0PPuS8xo4/P7/u+gH5gItmEVDFL6giYPFsPpqAQkUN7hFoGiVZEjO4PwrLOmydsEcNOfACqrnUs08FQtvPg0sjHnxh6nh6FUQv93ukKl6+c9d+pCsN2xukrQ7Dog3nrjFZ6PrS5J0k9rDAOwTB55sfGXPZ2rATOK1WS4XcpsCtqwnYm4sGNc8ALMQkQ97zCnw8TcQwLvdUMlfbqQ5ykDQpQD68fITEDDHmBAeTCjpC713E6AhvOMwTJvjhd7hSkeOTRTmn9zXIVGNo1jSr8u0xO9uLGeWsV0+UlRLgp7/nsgfermjwNN8wj6MW3DHGS8UzzYfe9TGCeywqqIUTqgfXY48leGgB7twh4cl4jcOQniLATTvigIAQIvq/Uv8L45BGnkpKTdQ5F73gehXdVA",
                    "type": 1
                }
            },
            "org.matrix.msgid": "93ee0170aa7740d0ac9ee137e820302d",
            "sender_key": alice_curve.to_base64(),
        },
        "type": "m.room.encrypted",
        "sender": "@alice:example.org",
    });

    let utd_event: Raw<AnyToDeviceEvent> = serde_json::from_value(utd_event).unwrap();

    let sync_changes = EncryptionSyncChanges {
        to_device_events: vec![encrypted_event, clear_event, malformed_event, utd_event],
        changed_devices: &Default::default(),
        one_time_keys_counts: &Default::default(),
        unused_fallback_keys: None,
        next_batch_token: None,
    };

    let decryption_settings =
        DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };

    let (processed, _) =
        bob.receive_sync_changes(sync_changes, &decryption_settings).await.unwrap();

    assert_eq!(4, processed.len());

    let processed_event = &processed[0];
    assert_matches!(processed_event, ProcessedToDeviceEvent::Decrypted { .. });

    insta::with_settings!({ prepend_module_to_snapshot => false }, {
        assert_json_snapshot!(
            processed_event.to_raw().deserialize_as::<Value>().unwrap(),
             {
                 ".keys.ed25519" => "[sender_ed25519_key]",
                 r#"["sender_device_keys"].device_id"# => "[ABCDEFGH]",
                 r#"["sender_device_keys"].keys"# => "++REDACTED++",
                 r#"["sender_device_keys"].signatures"# => "++REDACTED++",
                 // Redacted because depending on feature flags
                 r#"["sender_device_keys"].algorithms"# => "++REDACTED++",
                 ".recipient_keys.ed25519" => "[recipient_sender_key]",
            }
        );
    });

    let processed_event = &processed[1];
    assert_matches!(processed_event, ProcessedToDeviceEvent::PlainText(_));

    insta::with_settings!({ prepend_module_to_snapshot => false }, {
        assert_json_snapshot!(
            processed_event.to_raw().deserialize_as::<Value>().unwrap(),
        );
    });

    let processed_event = &processed[2];
    assert_matches!(processed_event, ProcessedToDeviceEvent::Invalid(_));

    insta::with_settings!({ prepend_module_to_snapshot => false }, {
        assert_json_snapshot!(
            processed_event.to_raw().deserialize_as::<Value>().unwrap(),
        );
    });

    let processed_event = &processed[3];
    assert_matches!(processed_event, ProcessedToDeviceEvent::UnableToDecrypt { utd_info, .. });
    assert_eq!(utd_info.reason, ToDeviceUnableToDecryptReason::DecryptionFailure);

    insta::with_settings!({ prepend_module_to_snapshot => false }, {
        assert_json_snapshot!(
            processed_event.to_raw().deserialize_as::<Value>().unwrap(),
            {
                ".content.sender_key" => "[sender_ed25519_key]",
                ".content.ciphertext" => "[++REDACTED++]",
            }
        );
    });
}

#[async_test]
async fn test_send_encrypted_to_device_no_session() {
    let (alice, bob, _) = get_machine_pair(tests::alice_id(), tests::user_id(), false).await;

    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    let encryption_result = alice
        .get_device(bob.user_id(), tests::bob_device_id(), None)
        .await
        .unwrap()
        .unwrap()
        .encrypt_event_raw(custom_event_type, &custom_content, CollectStrategy::AllDevices)
        .await;

    assert_matches!(encryption_result, Err(OlmError::MissingSession));
}

/// Create a new [`OutboundGroupSession`], and build a to-device event to share
/// it with another [`OlmMachine`], *without* sending the MSC4147 sender data.
///
/// # Arguments
///
/// * `alice` - sending device.
/// * `bob` - receiving device.
/// * `room_id` - room to create a session for.
async fn create_and_share_session_without_sender_data(
    alice: &OlmMachine,
    bob: &OlmMachine,
    room_id: &RoomId,
) -> ToDeviceEvent<ToDeviceEncryptedEventContent> {
    let (outbound_session, _) = alice
        .inner
        .group_session_manager
        .get_or_create_outbound_session(
            room_id,
            EncryptionSettings::default(),
            SenderData::unknown(),
        )
        .await
        .unwrap();

    // In future, we might want to save the session to the store, to better match
    // the behaviour of the real implementation. See
    // `GroupSessionManager::share_room_key` for inspiration on how to do that.

    let bob_device = alice
        .get_device(bob.user_id(), bob.device_id(), None)
        .await
        .unwrap()
        .expect("Attempt to send message to unknown device");
    let room_key_content = outbound_session.as_content().await;

    let content = build_encrypted_to_device_content_without_sender_data(
        alice,
        &bob_device.device_keys,
        room_key_content.event_type(),
        &room_key_content,
    )
    .await;

    ToDeviceEvent::new(alice.user_id().to_owned(), content)
}

/// Simulate uploading keys for alice that mean bob thinks alice's device
/// exists, but is unverified.
async fn make_alice_unverified(alice: &OlmMachine, bob: &OlmMachine) {
    let CrossSigningBootstrapRequests { upload_signing_keys_req: upload_signing, .. } =
        alice.bootstrap_cross_signing(false).await.expect("Expect Alice x-signing key request");

    let device_keys = alice
        .get_device(alice.user_id(), alice.device_id(), None)
        .await
        .unwrap()
        .unwrap()
        .as_device_keys()
        .to_owned();

    let updated_keys_with_x_signing = json!({ device_keys.device_id.to_string(): device_keys });

    let json = json!({
        "device_keys": {
            alice.user_id() : updated_keys_with_x_signing
        },
        "failures": {},
        "master_keys": {
            alice.user_id() : upload_signing.master_key.unwrap(),
        },
        "user_signing_keys": {
            alice.user_id() : upload_signing.user_signing_key.unwrap(),
        },
        "self_signing_keys": {
            alice.user_id() : upload_signing.self_signing_key.unwrap(),
        },
      }
    );

    let kq_response = ruma_response_from_json(&json);
    alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
    bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
}

#[async_test]
/// Test that when we get an error when we try to encrypt to a device that
/// doesn't satisfy the share strategy.
async fn test_share_strategy_prevents_encryption() {
    use matrix_sdk_common::deserialized_responses::WithheldCode;
    use matrix_sdk_test::test_json::keys_query_sets::KeyDistributionTestData as DataSet;
    use ruma::TransactionId;

    use crate::CrossSigningKeyExport;

    // Create the local user (`@me`), and import the public identity keys
    let machine = OlmMachine::new(DataSet::me_id(), DataSet::me_device_id()).await;
    let keys_query = DataSet::me_keys_query_response();
    machine.mark_request_as_sent(&TransactionId::new(), &keys_query).await.unwrap();

    // Also import the private cross signing keys
    machine
        .import_cross_signing_keys(CrossSigningKeyExport {
            master_key: DataSet::MASTER_KEY_PRIVATE_EXPORT.to_owned().into(),
            self_signing_key: DataSet::SELF_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
            user_signing_key: DataSet::USER_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
        })
        .await
        .unwrap();

    let keys_query = DataSet::dan_keys_query_response();
    let txn_id = TransactionId::new();
    machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();

    let custom_event_type = "m.new_device";

    let custom_content = json!({
            "device_id": "XYZABCDE",
            "rooms": ["!726s6s6q:example.com"]
    });

    let encryption_result = machine
        .get_device(DataSet::dan_id(), DataSet::dan_unsigned_device_id(), None)
        .await
        .unwrap()
        .unwrap()
        .encrypt_event_raw(custom_event_type, &custom_content, CollectStrategy::OnlyTrustedDevices)
        .await;

    assert_matches!(encryption_result, Err(OlmError::Withheld(WithheldCode::Unverified)));
}
